// (C) 2022 Intel Corporation. All rights reserved.
// Your use of Intel Corporation's design tools, logic functions and other 
// software and tools, and its AMPP partner logic functions, and any output 
// files from any of the foregoing (including device programming or simulation 
// files), and any associated documentation or information are expressly subject 
// to the terms and conditions of the Intel Program License Subscription 
// Agreement, Intel FPGA IP License Agreement, or other applicable 
// license agreement, including, without limitation, that your use is for the 
// sole purpose of programming logic devices manufactured by Intel and sold by 
// Intel or its authorized distributors.  Please refer to the applicable 
// agreement for further details.

module pca9555 #(parameter DEVICE_ADDR    = 7'h55,       //device address Master needs to use to access this device
                 parameter P_RV_OUTPUT0   = 8'hFF,       //Reset Value for OUTPUT0 register (addr 0x2). Default value according to PCA9554
                 parameter P_RV_POLARITY0 = 8'h00,       //Reset Value for POLARITY0 register (addr 0x4). Default value according to PCA9554
                 parameter P_RV_CONFIG0   = 8'hFF,       //Reset Value for CONFIG0 register (addr 0x6). Default value according to PCA9554
                 parameter P_RV_OUTPUT1   = 8'hFF,       //Reset Value for OUTPUT1 register (addr 0x3). Default value according to PCA9554
                 parameter P_RV_POLARITY1 = 8'h00,       //Reset Value for POLARITY1 register (addr 0x5). Default value according to PCA9554
                 parameter P_RV_CONFIG1   = 8'hFF,       //Reset Value for CONFIG1 register (addr 0x7). Default value according to PCA9554
                 parameter CC_FREQM       = 50
                 )
   (
    input       iClk,   //input clock for WDT (2MHz)
    input       iRst_n, //input reset signal (active low)
    input       iSCL, //input clock signal for I2C/SMBUS
    inout       ioSDA, //input data signal from

    inout [7:0] ioDataP0,
    inout [7:0] ioDataP1,

    output      oSmbAlert_n
   
    );

   /////////////////////////////////////////////////////////////////////////////////////////////////
   //local param declarations
   ///////////////////////////////////

   localparam WDRT = (CC_FREQM == 50) ? 50000 : 2000;
   
   
   //FSM STATES      
   localparam INIT = 0;
   localparam DEV_ADDR = 1;
   localparam ACK_ST = 2;
   localparam REG_ADDR = 3;
   localparam REG_WRITE = 4;
   localparam REG_READ = 5;
   localparam MASTER_ACK = 6;


   //register addresses
   localparam INPUT0 = 3'h0;
   localparam INPUT1 = 3'h1;
   localparam OUTPUT0 = 3'h2;
   localparam OUTPUT1 = 3'h3;
   localparam POLARITY0 = 3'h4;
   localparam POLARITY1 = 3'h5;
   localparam CONFIG0 = 3'h6;
   localparam CONFIG1 = 3'h7;

   /////////////////////////////////////////////////////////////////////////////////////////////////
   //Internal register declarations
   ///////////////////////////////////


   reg   rSDAo;                   //used to put the read data from regs (or the slave ACK) data
   
   
   reg   start_fsm;               //used to signal when a START condition has been detected
   reg   stop_fsm;                //used to signal when a STOP condition has been detected

   reg [2:0] rI2CFsm;             //I2C FSM, clocked by SCL input signal from link
   reg [2:0] fsm_prev_st;         //stores from where you are jumping into ACK_ST state
   

   reg [7:0] rDevAddr;            //Used to store the recieved Device Address from I2C/SMBus link. This needs
                                  //to be compared with current device address param to execute further or not
   
   reg [2:0] cnt;                 //used to parallelize/serialize data

   reg [7:0] rRegAddr;           //to store internal register address to be addressed

   reg       ack_ena;             //asserted when ACK should be sent to Master
   reg       read_ena;            //asserted during register read cycle. Together with reg_address will select the output data to be transmitted thru SDA inout

   reg [7:0] rInput0;             //these are the 8 registers (8bits) the PCA9555 has, from ADDR = 0 to 7
   reg [7:0] rInput1;

   reg [7:0] rInput0_t;           //bkup read data to know when there is a change on inputs to generate or not the alert_n signal 
   reg [7:0] rInput1_t;
   
   reg [7:0] rOutput0;            //write/read register thru I2C/SMBus I/F. to Port 0 output pins
   reg [7:0] rOutput1;            //write/read register thru I2C/SMBus I/F. to Port 1 output pins
   reg [7:0] rPolarity0;          //write/read register thru I2C/SMBus I/F. Configures polarity on how Port 0 input register should be read
   reg [7:0] rPolarity1;          //write/read register thru I2C/SMBus I/F. Configures polarity on how Port 1 input register should be read
   reg [7:0] rConfig0;            //write/read register thru I2C/SMBus I/F. Configures Port 0 IO bits as inputs (HIGH) or outputs (LOW)
   reg [7:0] rConfig1;            //write/read register thru I2C/SMBus I/F. Configures Port 1 IO bits as inputs (HIGH) or outputs (LOW)

   reg       proxy_bit;       //writes are performed to this bit, and it will pass to the right register filtered by the stop bit (if stop bit is asserted, proxy bit won't be written to internal register
   reg       filter_flag;     //when in the write_reg, we turn on this as soon as we receive the 1st data, flag is cleared either at ACK_ST or at INIT. This helps keeping input data to be then validated by !stop_fsm flag
                              //before it is written

   reg       read_flag;       //it is used to distinguish if it is the 1st byte or 2nd byte of the read command (to know where to end the read)
   
   reg       ack_out;         //indicates acknowledge is asserted and the inout data pin (ioSDA) is as output 
   reg       read_out;        //indicates data is being read and the inout data pin (ioSDA) is as output 
   

   reg       start_clr_n;     //after start condition is detected, as it's latched, needs to be cleared
   wire      start_rst_n;     //these 2 signals are for this purpose

   wire      wSmbAlert0_n;    //used to distinguish if any input changed its value and alert needs to be asserted
   wire      wSmbAlert1_n;

   wire      wDoneTimer1ms;
   
   
   reg [5:0] scl_t;
   reg [5:0] sda_t;
   reg       sda_synced;
   reg       scl_rise;
   reg       scl_fall;


   //////////////////////////

   always @(posedge iClk, negedge iRst_n)
     begin
        if (~iRst_n)
          begin
             scl_t <= 3'b111;
             scl_rise <= 1'b0;
             scl_fall <= 1'b0;

             sda_t <= 3'b111;
             
          end
        else
          begin
             scl_t[0] <= iSCL;
             scl_t[1] <= scl_t[0];
             scl_t[2] <= scl_t[1];
             scl_t[3] <= scl_t[2];
             scl_t[4] <= scl_t[3];
             scl_t[5] <= scl_t[4];
             scl_rise <= !scl_t[5] && scl_t[4];

             scl_fall <= scl_t[5] && !scl_t[4];

             ///

             sda_t[0] <= ioSDA;
             sda_t[1] <= sda_t[0];
             sda_t[2] <= sda_t[1];
             sda_t[3] <= sda_t[2];
             sda_t[4] <= sda_t[3];
             sda_t[5] <= sda_t[4];
             if (sda_t[5] == sda_t[4]) 
               sda_synced <= sda_t[5];
             
          end
     end

   
   
   ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////


   //1-msec Timer, started when read_out or ack_out are HIGH, clear when both low
   delay #(.COUNT(WDRT)) 
     Timer1ms(
               .iClk(iClk),
               .iRst(iRst_n),
               .iStart(read_out || ack_out),
               .iClrCnt(1'b0),
               .oDone(wDoneTimer1ms)
               );
  
   ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

   
   //registering the ioData Ports 0&1 into the respective input register port (all pins, as specified in the datasheet)
   always @(negedge iClk, negedge iRst_n)
     begin
        if (!iRst_n)
          begin
             rInput0 <= 8'h00;
             rInput1 <= 8'h00;
          end
        else
          begin
             rInput0 <= ioDataP0;
             rInput1 <= ioDataP1;
          end
     end


   //used as the reset for the start_fsm flip-flop, combining the regular Rst with the condition when the start was detected and processed
   //to avoid start indication to remain longer and be falsely seen after the current operation ends
   assign start_rst_n = !iRst_n || !start_clr_n;
   
   
   //This is to detect the start condition in the I2C/SMBUS link
   //We are using data line as the clock of the flip flop (negedge), and the clock as the enable to set a HIGH or LOW value at the outupt of it
   //The effect is that flip-flop output will be HIGH if a START condition is detected, LOW otherwise
   always @(negedge sda_synced, posedge start_rst_n)
     begin
        if (start_rst_n)
          begin
             start_fsm <= 1'b0;
          end
        else
          begin
             if (iSCL)
               begin
                  start_fsm <= 1'b1;                     //asserts when I2C start condition is detected: a change on data signal (from HIGH to LOW) while clk is HIGH
               end
          end // else: !if(~iRst_n)
     end // always @ (negedge ioSDA, negedge iRst_n)


   ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
   
   //This is to detect the stop condition in the I2C/SMBUS link
   //We are using data line as the clock of the flip flop (posedge), and the clock as the enable to set a HIGH or LOW value at the outupt of it
   //The effect is that flip-flop output will be HIGH if a STOP condition is detected, LOW otherwise
   always @(posedge sda_synced, posedge start_fsm)
     begin
        if (start_fsm)
          begin
             stop_fsm <= 1'b0;
          end
        else
          begin
             if (iSCL)
               begin
                  stop_fsm <= 1'b1;                     //asserts when I2C start condition is detected: a change on data signal (from HIGH to LOW) while clk is HIGH
               end
             else
               begin
                  stop_fsm <= 1'b0;                     //deasseted the rest of the times
               end
          end // else: !if(~iRst_n)
     end // always @ (negedge ioSDA, negedge iRst_n)
   


   ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
   //this is the main FSM for the I2C/SMBUS master and PCA9555 althogether
   //this has both the read control and the write portion
   //read control signals are used in another block (check below) 
   /////////////////////////////////////////////////////////////////////////
   
   always @(posedge iClk, negedge iRst_n)
     begin
        if (~iRst_n)
          begin
             rI2CFsm <= INIT;                           //INIT state waits for START condition to go forward
             fsm_prev_st <= INIT;
             rDevAddr <= 8'h00;
             rRegAddr <= 8'h00;
             cnt <= 3'h7;
             ack_ena <= 1'b0;
             read_ena <= 1'b0;
             rOutput0 <= P_RV_OUTPUT0;
             rOutput1 <= P_RV_OUTPUT1;
             rPolarity0 <= P_RV_POLARITY0;
             rPolarity1 <= P_RV_POLARITY1;
             rConfig0 <= P_RV_CONFIG0;
             rConfig1 <= P_RV_CONFIG1;
             start_clr_n <= 1'b1;
             proxy_bit <= 1'b0;
             filter_flag <= 1'b0;
             read_flag <= 1'b1;
          end
        else if (scl_rise)
          begin
             //if at any moment a STOP or a START are received, FSM needs to act properly, so these conditions are placed here to be checked at every cycle (every state)
             if (stop_fsm == 1'b1)        //stop indication received
               begin
                  rI2CFsm <= INIT;    
                  cnt <= 3'h7;
                  fsm_prev_st <= INIT;
                  rDevAddr <= 8'h00;
                  rRegAddr <= 8'h00;
                  cnt <= 3'h7;
                  ack_ena <= 1'b0;
                  read_ena <= 1'b0;
                  proxy_bit <= 1'b0;

                  
               end
             else if (start_fsm == 1'b1 && rI2CFsm != INIT)  //restart received
               begin
                  rDevAddr[7] <= sda_synced;
                  rI2CFsm <= DEV_ADDR;
                  fsm_prev_st <= DEV_ADDR;
                  cnt <= 3'h6;
                  start_clr_n <= 1'b0;
                  ack_ena <= 1'b0;
                  read_ena <= 1'b0;
               end
             else
               begin                      //FSM logic begins here
                  case (rI2CFsm)
                    INIT: begin
                       filter_flag <= 1'b0;
                       start_clr_n <= 1'b1;
                       ack_ena <= 1'b0;
                       read_ena <= 1'b0;
                       rDevAddr[cnt] <= sda_synced;
                       read_flag <= 1'b0;                    //clearing read flag, in case command is a read
                       proxy_bit <= 1'b1;
                       if (start_fsm == 1'b1)
                         begin
                            cnt <= cnt - 3'h1;
                            rI2CFsm <= DEV_ADDR;            //if START happened, go to DEV_ADDR state and decrease counter
                            start_clr_n <= 1'b0;            //start condition detected, clearing the flag
                            fsm_prev_st <= rI2CFsm;
                         end
                    end
                    
                    DEV_ADDR: begin
		               start_clr_n <= 1'b1;
                       rDevAddr[cnt] <= sda_synced;
                       if (cnt == 3'h0)
                         begin
                            cnt <= 3'h7;                            //last bit read, need to restart cnt value to 7
                            if (rDevAddr[7:1] == DEVICE_ADDR)    //if read 7-bit addr same as device addr, send ACK & continue with port address read
                              begin
                                 fsm_prev_st <= rI2CFsm;         //as we go to ACK state, we store from where we jumped, so we know where to go next
                                 rI2CFsm <= ACK_ST; 
                                 ack_ena <= 1'b1;                //enables the acknoledge to be set
                              end
                            else
                              begin
                                 rI2CFsm <= INIT;                //else, means bus access not for this device and need to clear up and
                                 rDevAddr <= 8'h00;              //go back to be ready for next cycle
                              end
                         end
                       else
                         begin
                            cnt <= cnt - 3'h1;                      //keep decrementing counter and storing ADDR bits in register
                            rI2CFsm <= DEV_ADDR;
                         end
                    end // case: DEV_ADDR
                    
                    ACK_ST:begin
                       ack_ena <= 1'b0;                        
                       case(fsm_prev_st)
                         DEV_ADDR: begin
                            if (rDevAddr[0] == 1'b0)          //write cycle
                              begin
                                 rI2CFsm <= REG_ADDR;            //then go to get register address state
                                 read_ena <= 1'b0;
                              end
                            else
                              begin
                                 rI2CFsm <= REG_READ;            //then go to get register address state
                                 read_ena <= 1'b1;
                              end
                         end
                         REG_ADDR: begin                    //if we were here, it is a write cycle
                            rI2CFsm <= REG_WRITE;           //then go to get register address state
                         end
                         REG_WRITE:begin
                            rI2CFsm <= REG_WRITE;             //if came from WRITE_REG st and no STOP nor START condition detected, keep go back to WRITE REG for more data to be read (and toggle LSb of reg-addr
                            filter_flag <= 1'b0;
                            rRegAddr[0] <= ~rRegAddr[0];      //if continue writing, change to the pair complement address
                            case (rRegAddr[2:0])              //depending on the RegAddr, start writing in that reg. We only have 8 valid addresses, we only use 3 addr bits and mask bigger addrs
                              OUTPUT0:begin
                                 rOutput0[0] <= proxy_bit;
                              end
                              OUTPUT1:begin
                                 rOutput1[0] <= proxy_bit;
                              end
                              POLARITY0:begin
                                 rPolarity0[0] <= proxy_bit;
                              end
                              POLARITY1:begin
                                 rPolarity1[0] <= proxy_bit;
                              end
                              CONFIG0:begin
                                 rConfig0[0] <= proxy_bit;
                              end
                              CONFIG1:begin
                                 rConfig1[0] <= proxy_bit;
                              end
                              default:begin
                                 
                              end
                            endcase // case (rRegAddr[2:0])
                         end // case: WRITE_REG
                         
                            
                         default: begin
                            rI2CFsm <= INIT;
                            cnt <= 3'h7;
                            rRegAddr <= 8'h00;
                            rDevAddr <= 8'h00;
                            start_clr_n <= 1'b0;
                         end
                       endcase // case (fsm_prev_st)
                    end // case: ACK_ST

                    
                    REG_ADDR: begin                        //need to gather port address to know to which port will address
                       rRegAddr[cnt] <= sda_synced;
                       if (cnt == 3'h0)                    //if all bits are read, go to ack state
                         begin                             
                            cnt <= 3'h7;                  
                            ack_ena <= 1'b1;
                            fsm_prev_st <= rI2CFsm;        //preserve present state in fsm_prev_st to know where to go next after ack
                            rI2CFsm <= ACK_ST;
                         end
                       else
                         begin
                            ack_ena <= 1'b0;
                            cnt <= cnt - 3'h1;
                            rI2CFsm <= REG_ADDR;
                         end
                    end // case: REG_ADDR

                    
                    REG_WRITE: begin
                       proxy_bit <= sda_synced;
                       filter_flag <= 1'b1;
                       
                       if (cnt == 3'h0)
                         begin
                            cnt <= 3'h7;
                            rI2CFsm <= ACK_ST;
                            fsm_prev_st <= rI2CFsm;
                            ack_ena <= 1'b1;
                         end
                       else
                         begin
                            cnt <= cnt - 3'h1;
                            rI2CFsm <= REG_WRITE;
                         end // else: !if(cnt == 3'h0)

                       if (filter_flag && !stop_fsm) begin
                          case (rRegAddr)
                            OUTPUT0: begin                   
                               rOutput0[cnt + 1] <= proxy_bit;             //real write happens to the register from proxy_bit if stop bit was not asserted
                            end
                            
                            OUTPUT1: begin
                               rOutput1[cnt + 1] <=  proxy_bit;
                            end
                            
                            POLARITY0: begin
                               rPolarity0[cnt + 1] <= proxy_bit;
                            end
                         
                            POLARITY1:begin
                               rPolarity1[cnt + 1] <= proxy_bit;
                            end
                            CONFIG0:begin
                               rConfig0[cnt + 1] <= proxy_bit;
                            end
                            CONFIG1:begin
                               rConfig1[cnt + 1] <= proxy_bit;
                            end

                            default:begin
                               
                            end
                          endcase // case (rRegAddr)
                       end // if (filter_flag)
                    end // case: REG_WRITE
   
             
             
                    REG_READ: begin
                       if (cnt == 3'h0)
                         begin
                            cnt <= 3'h7;
                            rI2CFsm <= MASTER_ACK;
                            read_ena <= 1'b0;
                         end
                       else
                         begin
                            cnt <= cnt - 3'h1;
                            read_ena <= 1'b1;
                         end
                    end // case: REG_READ


                    MASTER_ACK: begin
                       if (sda_synced) begin
                          rI2CFsm <= INIT;
                          read_ena <= 1'b0;
                          start_clr_n <= 1'b0;
                          
                       end
                       else begin
                          rRegAddr[0] <= ~rRegAddr[0];          //master acknowledge received, toggling LSb of register address to send next register data
                          rI2CFsm <= REG_READ;                  //if we do not get the NACK from Master, we do a continuous read by going back to REG_READ and we switch to the pair address
                          read_ena <= 1'b1;                     
                       end
                    end //case: MASTER_ACK

                    
                    default:begin
                       cnt <= 3'h7;
                       rI2CFsm <= INIT;              //going back to INIT state
                       rRegAddr <= 8'h00;
                       rDevAddr <= 8'h00;
                       start_clr_n <= 1'b0;
                    end
                    
                  endcase // case (rI2CFsm)
               end // else: !if(start_fsm == 1'b1)
          end // else: !if(~iRst_n)
     end // always @ (posedge iSCL, negedge iRst_n)

   wire read_rst_n;
   assign read_rst_n = iRst_n && !wDoneTimer1ms;

   ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
   //this logic helps to output data into SDA signal using falling edge of the SCL clock signal (read data)
   //however all control signals come from our main FSM clocked with the same SCL clock signal, but with the rising edge
   //////////////////////////////////////////////////////////////////
   
   always @(posedge iClk, negedge read_rst_n)
     begin
        if (~read_rst_n)
          begin
             rSDAo <= 1'b1;
             rInput0_t <= 8'h00;                   //reset value the same as rInput0/rInput1 respectively
             rInput1_t <= 8'h00;
             ack_out <= 1'b0;
             read_out <= 1'b0;
          end
        else if (scl_fall)
          begin
             if (ack_ena == 1'b1)
               begin
                  rSDAo <= 1'b0;                   //acknoledge to master
                  ack_out <= 1'b1;                 //this enables inout pin as output for the acknowledge bit to be seen by master
               end
             else if (read_ena && !stop_fsm)
               begin
                  read_out <= 1'b1;               //this enables inout pin as output for reading data to be seen by master
                  ack_out <= 1'b0;

                  case(rRegAddr[2:0])             //decoding reading internal address and getting the right data to the inout pin
                    INPUT0: begin
                       rSDAo <= rPolarity0[cnt] ^ rInput0[cnt];          //input is xored with Polarity reg, if Polarity == 1, input is read in inverted fashion, otherwise, normal
                       rInput0_t[cnt] <= rInput0[cnt];                   //bkup read data to know when there is a change on inputs to generate or not the alert_n signal 
                    end

                    INPUT1: begin
                       rSDAo <= rPolarity1[cnt] ^ rInput1[cnt];
                       rInput1_t[cnt] <= rInput1[cnt];
                    end

                    OUTPUT0: begin
                       rSDAo <= rOutput0[cnt];
                    end

                    OUTPUT1: begin
                       rSDAo <= rOutput1[cnt];
                    end
                    
                    POLARITY0: begin
                       rSDAo <= rPolarity0[cnt];
                    end

                    POLARITY1: begin
                       rSDAo <= rPolarity1[cnt];
                    end

                    CONFIG0: begin
                       rSDAo <= rConfig0[cnt];
                    end

                    CONFIG1: begin
                       rSDAo <= rConfig1[cnt];
                    end
                    
                    default:begin
                       rSDAo <= rSDAo;
                    end
                  endcase // case (rRegAddr[2:0])
               end // if (read_ena == 1'b1)
             else 
               begin
                  rSDAo <= 1'b1;                    //if no read operation, the pin is released (by driving a HIGH which will be converted into HIGHZ
                  ack_out <= 1'b0;                  //no enable is asserted
                  read_out <= 1'b0;
               end // else: !if(read_ena == 1'b1)
             
          end // if (scl_fall)
     end // always @ (negedge iSCL, negedge iRst_n)
      
                                                                                             //if read cycle or slave ack enable is asserted, then the output should be driving what the rSDAo has
   assign ioSDA = (read_out || ack_out) ? (rSDAo ? 1'bZ: 1'b0) : 1'bZ;                        //considering always a HIGH value should be HIGHZ to the output so we have an open-drain signal
   
   
   //output registers assigned to their respective outputs


   genvar idx;  //generate variable
   
   generate
      for (idx=0; idx<=7; idx=idx + 1)
        begin : output_buff
           assign ioDataP0[idx] = ~rConfig0[idx] ? rOutput0[idx] : 1'bZ;
           assign ioDataP1[idx] = ~rConfig1[idx] ? rOutput1[idx] : 1'bZ;
        end
   endgenerate

   
   assign wSmbAlert0_n = ((rInput0_t & rConfig0) == (ioDataP0 & rConfig0)) ? 1'b1 : 1'b0;     //if input data (read from pins) is different that last read data, need to assert an alert id to SMB Master
   assign wSmbAlert1_n = ((rInput1_t & rConfig1) == (ioDataP1 & rConfig1)) ? 1'b1 : 1'b0;     //if data register is read, t reg is updated and alert should be de-asserted.
                                                                                                //if input is changed back to previous state before it is read, alert is also de-asserted  

   assign oSmbAlert_n = (wSmbAlert0_n) ? 1'bZ : 1'b0;              //as we have 2 registers here, we check on both and merge info into 1 output
   

   
endmodule // pca9555
